import { GlAlert, GlModal, GlFormTextarea } from '@gitlab/ui';
import Vue, { nextTick } from 'vue';
import VueApollo from 'vue-apollo';
import waitForPromises from 'helpers/wait_for_promises';
import createMockApollo from 'helpers/mock_apollo_helper';
import { shallowMountExtended, extendedWrapper } from 'helpers/vue_test_utils_helper';
import { stubComponent, RENDER_ALL_SLOTS_TEMPLATE } from 'helpers/stub_component';
import VulnerabilityFindingModal, {
  STATE_DETECTED,
  STATE_DISMISSED,
  STATE_RESOLVED,
  VULNERABILITY_POLLING_INTERVAL,
} from 'ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue';
import download from '~/lib/utils/downloader';
import { visitUrl } from '~/lib/utils/url_utility';
import { ESC_KEY } from '~/lib/utils/keys';
import SolutionCard from 'ee/vue_shared/security_reports/components/solution_card_graphql.vue';
import IssueNote from 'ee/vue_shared/security_reports/components/issue_note_graphql.vue';
import MergeRequestNote from 'ee/vue_shared/security_reports/components/merge_request_note_graphql.vue';
import DismissalNote from 'ee/vue_shared/security_reports/components/dismissal_note.vue';
import EventItem from 'ee/vue_shared/security_reports/components/event_item.vue';
import SplitButton from 'ee/vue_shared/security_reports/components/split_button.vue';
import VulnerabilityDetailsGraphql from 'ee/security_dashboard/components/shared/vulnerability_details_graphql/index.vue';
import securityReportFindingQuery from 'ee/security_dashboard/graphql/queries/security_report_finding.query.graphql';
import vulnerabilityExternalIssuesQuery from 'ee/security_dashboard/graphql/queries/vulnerability_external_issues.query.graphql';
import dismissFindingMutation from 'ee/security_dashboard/graphql/mutations/dismiss_finding.mutation.graphql';
import createMergeRequestMutation from 'ee/security_dashboard/graphql/mutations/finding_create_merge_request.mutation.graphql';
import securityFindingRevertToDetected from 'ee/security_dashboard/graphql/mutations/revert_finding_to_detected.mutation.graphql';
import createIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_issue.mutation.graphql';
import createExternalIssueMutation from 'ee/security_dashboard/graphql/mutations/finding_create_jira_issue.mutation.graphql';
import {
  getPipelineSecurityReportFindingResponse,
  getVulnerabilityExternalIssuesQueryResponse,
  pipelineSecurityReportFinding,
  securityFindingDismissMutationResponse,
  securityFindingRevertToDetectedMutationResponse,
  securityFindingCreateMergeRequestMutationResponse,
  securityFindingCreateIssueMutationResponse,
  securityFindingCreateExternalIssueMutationResponse,
} from './mock_data';

jest.mock('~/lib/utils/downloader');
jest.mock('~/lib/utils/url_utility');

Vue.use(VueApollo);

const TEST_FINDING = pipelineSecurityReportFinding;
const TEST_PIPELINE_IID = 1;
const TEST_PROJECT_FULL_PATH = 'path/to/my/project';

describe('ee/security_dashboard/components/pipeline/vulnerability_finding_modal.vue', () => {
  let wrapper;
  let modalMockMethods;

  const createMockApolloProvider = ({ handlers = {} } = {}) => {
    const requestHandlers = [
      [
        securityReportFindingQuery,
        handlers.securityReportFindingQuery ||
          jest.fn().mockResolvedValue(getPipelineSecurityReportFindingResponse()),
      ],
      [
        vulnerabilityExternalIssuesQuery,
        handlers.vulnerabilityExternalIssuesQuery ||
          jest.fn().mockResolvedValue(getVulnerabilityExternalIssuesQueryResponse()),
      ],
      [
        dismissFindingMutation,
        handlers.dismissMutation ||
          jest.fn().mockResolvedValue(securityFindingDismissMutationResponse),
      ],
      [
        securityFindingRevertToDetected,
        handlers.revertToDetectedMutation ||
          jest.fn().mockResolvedValue(securityFindingRevertToDetectedMutationResponse),
      ],
      [
        createMergeRequestMutation,
        handlers.createMergeRequestMutation ||
          jest.fn().mockResolvedValue(securityFindingCreateMergeRequestMutationResponse),
      ],
      [
        createIssueMutation,
        handlers.createIssueMutation ||
          jest.fn().mockResolvedValue(securityFindingCreateIssueMutationResponse),
      ],
      [
        createExternalIssueMutation,
        handlers.createExternalIssueMutation ||
          jest.fn().mockResolvedValue(securityFindingCreateExternalIssueMutationResponse),
      ],
    ];

    return createMockApollo(requestHandlers);
  };

  const createWrapper = ({ responseHandlers, propsData } = {}) => {
    modalMockMethods = {
      hide: jest.fn(),
      setFocus: jest.fn(),
    };
    wrapper = shallowMountExtended(VulnerabilityFindingModal, {
      propsData: {
        findingUuid: TEST_FINDING.uuid,
        pipelineIid: TEST_PIPELINE_IID,
        projectFullPath: TEST_PROJECT_FULL_PATH,
        ...propsData,
      },
      stubs: {
        GlModal: stubComponent(GlModal, {
          template: RENDER_ALL_SLOTS_TEMPLATE,
          methods: modalMockMethods,
        }),
        SplitButton,
      },
      apolloProvider: createMockApolloProvider({
        handlers: responseHandlers,
      }),
    });
  };

  const findModal = () => wrapper.findComponent(GlModal);
  const findVulnerabilityDetails = () => wrapper.findComponent(VulnerabilityDetailsGraphql);
  const findErrorAlert = () => wrapper.findComponent(GlAlert);
  const findFooter = () => wrapper.findByTestId('footer');
  const withinFooter = () => extendedWrapper(findFooter());
  const findLoadingIndicators = () => [
    wrapper.findByTestId('title-loading-indicator'),
    wrapper.findByTestId('content-loading-indicator'),
  ];
  const findDismissButton = () => withinFooter().findByTestId('dismiss-button');
  const findCancelButton = () => withinFooter().findByTestId('cancel-button');
  const findCommentAndDismissButton = () => wrapper.findByTestId('dismiss-with-comment-button');
  const findDismissalCommentSection = () => wrapper.findByTestId('dismissal-comment-section');
  const findCommentInput = () => findDismissalCommentSection().findComponent(GlFormTextarea);
  const findDismissalNote = () => wrapper.findComponent(DismissalNote);

  const toggleFindingState = () => findDismissButton().vm.$emit('click');

  const expectModalToBeHiddenAfter = async ({ action }) => {
    expect(modalMockMethods.hide).not.toHaveBeenCalled();

    await action();

    expect(modalMockMethods.hide).toHaveBeenCalled();
  };

  const waitForFindingToBeLoaded = waitForPromises;
  const waitForFindingToBeDismissed = waitForPromises;

  describe('modal instance', () => {
    beforeEach(() => {
      createWrapper();
    });

    it('gets passed the correct props', () => {
      expect(findModal().props()).toMatchObject({
        modalId: expect.any(String),
      });
    });

    it('makes the component emit "hidden" when the modal gets closed', () => {
      expect(wrapper.emitted('hidden')).toBeUndefined();

      findModal().vm.$emit('hidden');

      expect(wrapper.emitted('hidden')).toHaveLength(1);
    });

    describe('footer', () => {
      it('renders as expected', () => {
        expect(findFooter().exists()).toBe(true);
      });

      it('contains a "cancel" button that will hide the modal', () => {
        expectModalToBeHiddenAfter({
          action: () => {
            findCancelButton().vm.$emit('click');
          },
        });
      });

      it('contains a "dismiss" button', () => {
        expect(findDismissButton().exists()).toBe(true);
      });
    });
  });

  describe('when loading', () => {
    beforeEach(() => {
      createWrapper();
    });

    it('does not show an error alert', () => {
      expect(findErrorAlert().exists()).toBe(false);
    });

    it('shows a skeleton loaders', () => {
      findLoadingIndicators().forEach((loadingIndicator) => {
        expect(loadingIndicator.exists()).toBe(true);
      });
    });
  });

  describe('when loaded successfully', () => {
    beforeEach(async () => {
      createWrapper();
      await waitForFindingToBeLoaded();
    });

    it('does not show an error alert', () => {
      expect(findErrorAlert().exists()).toBe(false);
    });

    it('does not show skeleton loaders', () => {
      findLoadingIndicators().forEach((loadingIndicator) => {
        expect(loadingIndicator.exists()).toBe(false);
      });
    });

    it(`shows the finding's title within the modal's header`, () => {
      expect(wrapper.findByRole('heading').text()).toBe(TEST_FINDING.title);
    });

    describe('finding details', () => {
      it('displays details about the given vulnerability finding', () => {
        const { description, severity } = TEST_FINDING;

        expect(findVulnerabilityDetails().props()).toMatchObject({
          description,
          severity,
        });
      });
    });

    describe('solution card', () => {
      it('gets passed the correct solution prop', () => {
        const {
          solution,
          remediations,
          vulnerability: { mergeRequest },
        } = TEST_FINDING;

        expect(wrapper.findComponent(SolutionCard).props()).toMatchObject({
          remediation: remediations[0],
          solution,
          mergeRequest,
        });
      });
    });

    describe('issue note', () => {
      it('gets passed the correct prop', () => {
        const { issueLinks, project } = TEST_FINDING;

        expect(wrapper.findComponent(IssueNote).props()).toMatchObject({
          issueLinks: issueLinks.nodes,
          project,
        });
      });
    });

    describe('merge request note', () => {
      it('gets passed the correct prop', () => {
        const {
          vulnerability: { mergeRequest },
          project,
        } = TEST_FINDING;

        expect(wrapper.findComponent(MergeRequestNote).props()).toMatchObject({
          mergeRequest,
          project,
        });
      });
    });
  });

  describe.each`
    description         | handlers
    ${'error response'} | ${jest.fn().mockRejectedValue()}
    ${'empty data'}     | ${jest.fn().mockResolvedValue(getPipelineSecurityReportFindingResponse({ withoutFindingData: true }))}
  `('with $description', ({ handlers }) => {
    beforeEach(async () => {
      createWrapper({
        responseHandlers: { securityReportFindingQuery: handlers },
      });
      await waitForFindingToBeLoaded();
    });

    it(`shows an error message within the modal's heading`, () => {
      expect(wrapper.findByRole('heading').text()).toBe('Error');
    });

    it('shows an error alert with the correct error message', () => {
      expect(findErrorAlert().text()).toBe(
        'There was an error fetching the finding. Please try again.',
      );
    });
  });

  describe('dismissal', () => {
    describe('state toggle', () => {
      it.each`
        initialState       | expectedNewState
        ${STATE_DISMISSED} | ${STATE_DETECTED}
        ${STATE_DETECTED}  | ${STATE_DISMISSED}
      `(
        "updates to '$expectedNewState' when initial state is '$initialState'",
        async ({ initialState, expectedNewState }) => {
          const firstResponse = getPipelineSecurityReportFindingResponse({
            overrides: {
              state: initialState,
            },
          });
          const secondResponse = getPipelineSecurityReportFindingResponse({
            overrides: {
              state: expectedNewState,
            },
          });
          const mockHandler = jest
            .fn()
            .mockResolvedValueOnce(firstResponse)
            .mockResolvedValueOnce(secondResponse);

          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: mockHandler,
            },
          });
          await waitForFindingToBeLoaded();

          expect(findVulnerabilityDetails().props()).toMatchObject({
            state: initialState,
          });

          toggleFindingState();
          await waitForFindingToBeDismissed();

          expect(findVulnerabilityDetails().props()).toMatchObject({
            state: expectedNewState,
          });
        },
      );
    });

    describe('success', () => {
      it.each`
        initialState       | expectedEventEmitted
        ${STATE_DISMISSED} | ${'detected'}
        ${STATE_DETECTED}  | ${'dismissed'}
      `(
        'emits "$expectedEventEmitted" when the initial state is "$initialState" and the state gets toggled',
        async ({ initialState, expectedEventEmitted }) => {
          const response = getPipelineSecurityReportFindingResponse({
            overrides: {
              state: initialState,
            },
          });
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(response),
            },
          });
          await waitForFindingToBeLoaded();

          expect(wrapper.emitted(expectedEventEmitted)).toBeUndefined();

          toggleFindingState();
          await waitForFindingToBeDismissed();

          expect(wrapper.emitted(expectedEventEmitted)).toBeDefined();
        },
      );

      it('hides the modal', () => {
        createWrapper();
        expectModalToBeHiddenAfter({
          action: async () => {
            findDismissButton().vm.$emit('click');
            await waitForFindingToBeDismissed();
          },
        });
      });
    });

    describe('error', () => {
      beforeEach(async () => {
        createWrapper({
          responseHandlers: {
            dismissMutation: jest.fn().mockRejectedValue(),
          },
        });
        await waitForFindingToBeLoaded();
      });

      it.each([
        {
          initialState: STATE_DETECTED,
          expectedErrorMessage: 'There was an error dismissing the finding. Please try again.',
        },
        {
          initialState: STATE_DISMISSED,
          expectedErrorMessage: 'There was an error reverting the dismissal.',
        },
      ])(
        'shows an error alert when the state mutation fails: %s',
        async ({ initialState, expectedErrorMessage }) => {
          const response = getPipelineSecurityReportFindingResponse({
            overrides: {
              state: initialState,
            },
          });
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(response),
              dismissMutation: jest.fn().mockRejectedValue(),
              revertToDetectedMutation: jest.fn().mockRejectedValue(),
            },
          });
          await waitForFindingToBeLoaded();
          expect(findErrorAlert().exists()).toBe(false);

          toggleFindingState();
          await waitForFindingToBeDismissed();

          expect(findErrorAlert().text()).toBe(expectedErrorMessage);
        },
      );
    });

    describe('commenting', () => {
      describe('without existing feedback', () => {
        const TEST_CURRENT_USER_ID = '1';
        const TEST_CURRENT_USER_NAME = 'root';
        const TEST_CURRENT_USER_FULL_NAME = 'root user';

        const mockDismissFindingMutation = jest.fn().mockResolvedValue({
          data: {
            securityFindingDismiss: {
              errors: [],
            },
          },
        });

        beforeEach(() => {
          window.gon = {
            current_user_id: TEST_CURRENT_USER_ID,
            current_username: TEST_CURRENT_USER_NAME,
            current_user_fullname: TEST_CURRENT_USER_FULL_NAME,
          };
          createWrapper({
            responseHandlers: {
              dismissMutation: mockDismissFindingMutation,
            },
          });
        });

        it('does not show the dismissal comment section', () => {
          expect(findDismissalCommentSection().exists()).toBe(false);
        });

        it('does not render the dismissal notes section', async () => {
          createWrapper();
          expect(findDismissalNote().exists()).toBe(false);

          await findCommentAndDismissButton().vm.$emit('click');
          // Should also not render when adding a dismissal comment
          expect(findDismissalNote().exists()).toBe(false);
        });

        it('contains a button to add a comment and dismiss', () => {
          expect(findCommentAndDismissButton().attributes()).toMatchObject({
            title: 'Add comment & dismiss',
          });
        });

        describe('when the "Add comment & dismiss" split-button is clicked', () => {
          beforeEach(async () => {
            findCommentAndDismissButton().vm.$emit('click');
            await nextTick();
          });

          it('should show the dismissal comment section', () => {
            expect(findDismissalCommentSection().exists()).toBe(true);
          });

          it('should hide split-button', () => {
            expect(findCommentAndDismissButton().exists()).toBe(false);
          });

          it('should change the text of the dismissal button', () => {
            expect(findDismissButton().text()).toBe('Add comment & dismiss');
          });

          describe('comment section', () => {
            it('shows information about the current user', () => {
              expect(findDismissalCommentSection().findComponent(EventItem).props()).toMatchObject({
                author: {
                  id: TEST_CURRENT_USER_ID,
                  username: TEST_CURRENT_USER_NAME,
                  name: TEST_CURRENT_USER_FULL_NAME,
                  state: 'active',
                },
              });
            });

            it('contains a text area that receives auto focus and has the correct placeholder', () => {
              expect(findCommentInput().attributes('autofocus')).not.toBe(undefined);
              expect(findCommentInput().attributes('placeholder')).toBe(
                'Add a comment or reason for dismissal',
              );
            });

            it('closes the comment section when the "Esc" key is pressed', async () => {
              expect(findDismissalCommentSection().exists()).toBe(true);
              expect(modalMockMethods.setFocus).not.toHaveBeenCalled();

              findCommentInput().vm.$emit(
                'keydown',
                new KeyboardEvent('keydown', { key: ESC_KEY }),
              );
              await nextTick();

              expect(modalMockMethods.setFocus).toHaveBeenCalled();
              expect(findDismissalCommentSection().exists()).toBe(false);
            });

            it('shows an input to enter a comment and dismiss with it', async () => {
              expect(mockDismissFindingMutation).not.toHaveBeenCalled();

              const comment = 'dismissed because the finding is a false-positive';
              findCommentInput().vm.$emit('input', comment);

              findDismissButton().vm.$emit('click');
              await waitForPromises();

              expect(mockDismissFindingMutation).toHaveBeenCalledTimes(1);
              const [firstCall] = mockDismissFindingMutation.mock.calls;
              expect(firstCall[0].comment).toBe(comment);
            });

            it('cancels commenting', async () => {
              expect(findDismissalCommentSection().exists()).toBe(true);

              findCancelButton().vm.$emit('click');
              await nextTick();

              expect(findDismissalCommentSection().exists()).toBe(false);
            });
          });
        });
      });

      describe('with existing feedback', () => {
        const TEST_DISMISSED_AT = '2022-10-16T22:42:02.975Z';
        const TEST_STATE_COMMENT = 'false positive';
        const TEST_DISMISSED_BY = {
          id: 1,
          name: 'Admin',
          username: 'admin',
          webUrl: 'http://gitlab.com/admin',
        };
        const TEST_DISMISSAL_REASON = 'MITIGATING_CONTROL';
        const TEST_PROJECT = {
          webUrl: 'http://gitlab.com/gitlab-org/security/gitlab',
          nameWithNamespace: 'GitLab/Security/GitLab',
        };

        beforeEach(async () => {
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(
                getPipelineSecurityReportFindingResponse({
                  overrides: {
                    project: TEST_PROJECT,
                    state: STATE_DISMISSED,
                    dismissedAt: TEST_DISMISSED_AT,
                    stateComment: TEST_STATE_COMMENT,
                    dismissedBy: TEST_DISMISSED_BY,
                    dismissalReason: TEST_DISMISSAL_REASON,
                  },
                }),
              ),
            },
          });
          await waitForPromises();
        });

        it('renders dismissal notes', () => {
          expect(findDismissalNote().props()).toMatchObject({
            project: {
              url: TEST_PROJECT.webUrl,
              value: TEST_PROJECT.nameWithNamespace,
            },
            feedback: {
              created_at: TEST_DISMISSED_AT,
              comment_details: {
                comment_author: TEST_DISMISSED_BY,
                comment: TEST_STATE_COMMENT,
              },
              author: TEST_DISMISSED_BY,
              dismissalReason: TEST_DISMISSAL_REASON,
            },
          });
        });

        it('shows "cancel" and "save" buttons when editing the comment', async () => {
          const findCancelEditButton = () => withinFooter().findByTestId('cancel-editing-comment');
          const findSaveEditedButton = () => withinFooter().findByTestId('save-edited-comment');

          expect(findCancelEditButton().exists()).toBe(false);
          expect(findSaveEditedButton().exists()).toBe(false);

          findDismissalNote().vm.$emit('editVulnerabilityDismissalComment');
          await nextTick();

          expect(findCancelEditButton().exists()).toBe(true);
          expect(findSaveEditedButton().exists()).toBe(true);
        });

        it('allows the existing comment to be edited', async () => {
          expect(findDismissalNote().props('isCommentingOnDismissal')).toBe(false);

          findDismissalNote().vm.$emit('editVulnerabilityDismissalComment');
          await nextTick();

          expect(findDismissalNote().props('isCommentingOnDismissal')).toBe(true);
        });

        it('shows and hides the delete buttons', async () => {
          expect(findDismissalNote().props('isShowingDeleteButtons')).toBe(false);

          findDismissalNote().vm.$emit('showDismissalDeleteButtons');
          await nextTick();

          expect(findDismissalNote().props('isShowingDeleteButtons')).toBe(true);

          findDismissalNote().vm.$emit('hideDismissalDeleteButtons');
          await nextTick();

          expect(findDismissalNote().props('isShowingDeleteButtons')).toBe(false);
        });
      });
    });
  });

  describe('footer actions', () => {
    const findActionButtons = () => wrapper.findByTestId('footer-action-buttons');
    const findDownloadPatchButton = () => wrapper.findByTestId('download-patch-button');
    const findCreateMergeRequestButton = () => wrapper.findByTestId('create-merge-request-button');

    describe('without remediations', () => {
      beforeEach(async () => {
        createWrapper({
          responseHandlers: {
            securityReportFindingQuery: jest.fn().mockResolvedValue(
              getPipelineSecurityReportFindingResponse({
                overrides: {
                  remediations: [],
                },
              }),
            ),
          },
        });
        await waitForPromises();
      });

      it('should not show the download patch button', () => {
        expect(findDownloadPatchButton().exists()).toBe(false);
      });

      it('should not show the create merge request button', () => {
        expect(findCreateMergeRequestButton().exists()).toBe(false);
      });
    });

    describe('with remediations', () => {
      const TEST_REMEDIATION = { diff: 'SGVsbG8gR2l0TGFi', summary: 'Upgrade libcurl' };

      describe('when finding is resolved', () => {
        beforeEach(async () => {
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(
                getPipelineSecurityReportFindingResponse({
                  overrides: {
                    state: STATE_RESOLVED,
                    remediations: [TEST_REMEDIATION],
                  },
                }),
              ),
            },
          });
          await waitForPromises();
        });

        it('should not show the download patch button', () => {
          expect(findDownloadPatchButton().exists()).toBe(false);
        });

        it('should not show the create merge request button', () => {
          expect(findCreateMergeRequestButton().exists()).toBe(false);
        });
      });

      describe('when finding has an existing merge request', () => {
        beforeEach(async () => {
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(
                getPipelineSecurityReportFindingResponse({
                  overrides: {
                    state: STATE_DETECTED,
                    mergeRequest: {
                      iid: '1',
                      webUrl: 'http://gitlab.com/merge-request',
                    },
                    remediations: [TEST_REMEDIATION],
                  },
                }),
              ),
            },
          });
          await waitForPromises();
        });

        it('should not show the download patch button', () => {
          expect(findDownloadPatchButton().exists()).toBe(false);
        });

        it('should not show the create merge request button', () => {
          expect(findCreateMergeRequestButton().exists()).toBe(false);
        });
      });

      describe('when the finding is unresolved an there is no existing merge request', () => {
        describe.each`
          hasDiff  | shouldShowDownloadPatch | shouldShowCreateMergeRequest
          ${true}  | ${true}                 | ${true}
          ${false} | ${false}                | ${true}
        `(
          'the finding has diff data is "$hasDiff"',
          ({ hasDiff, shouldShowDownloadPatch, shouldShowCreateMergeRequest }) => {
            beforeEach(async () => {
              createWrapper({
                responseHandlers: {
                  securityReportFindingQuery: jest.fn().mockResolvedValue(
                    getPipelineSecurityReportFindingResponse({
                      overrides: {
                        state: STATE_DETECTED,
                        vulnerability: {
                          mergeRequest: null,
                        },
                        remediations: [
                          {
                            diff: hasDiff ? TEST_REMEDIATION.diff : null,
                          },
                        ],
                      },
                    }),
                  ),
                },
              });

              await waitForPromises();
            });

            it(`${
              shouldShowDownloadPatch ? 'should' : 'should not'
            } render the download-patch button`, () => {
              expect(findDownloadPatchButton().exists()).toBe(shouldShowDownloadPatch);
            });

            it(`${
              shouldShowCreateMergeRequest ? 'should' : 'should not'
            } render the create-merge-request button`, () => {
              expect(findCreateMergeRequestButton().exists()).toBe(shouldShowCreateMergeRequest);
            });
          },
        );
      });

      describe('download patch button', () => {
        beforeEach(async () => {
          createWrapper({
            responseHandlers: {
              securityReportFindingQuery: jest.fn().mockResolvedValue(
                getPipelineSecurityReportFindingResponse({
                  overrides: {
                    vulnerability: {
                      mergeRequest: null,
                    },
                    remediations: [TEST_REMEDIATION],
                  },
                }),
              ),
            },
          });

          await waitForPromises();
        });

        it('should trigger a file-download with the remediation diff when the download button is clicked', () => {
          expect(download).not.toHaveBeenCalled();

          findActionButtons().vm.$emit('download-patch');

          expect(download).toHaveBeenCalledWith({
            fileData: TEST_REMEDIATION.diff,
            fileName: 'remediation.patch',
          });
        });
      });

      describe('create merge request', () => {
        const TEST_MERGE_REQUEST = { id: '1', iid: '1', webUrl: 'https://gitlab.com' };
        const createMergeRequestMutationHandler = jest.fn().mockResolvedValue({
          data: {
            securityFindingCreateMergeRequest: {
              errors: [],
              mergeRequest: TEST_MERGE_REQUEST,
            },
          },
        });

        describe('success', () => {
          beforeEach(async () => {
            createWrapper({
              responseHandlers: {
                createMergeRequestMutation: createMergeRequestMutationHandler,
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      vulnerability: {
                        mergeRequest: null,
                      },
                      remediations: [TEST_REMEDIATION],
                    },
                  }),
                ),
              },
            });

            await waitForPromises();
          });

          it('should trigger a create merge request mutation when the button is clicked', () => {
            expect(createMergeRequestMutationHandler).not.toHaveBeenCalled();

            findActionButtons().vm.$emit('create-merge-request');

            expect(createMergeRequestMutationHandler).toHaveBeenCalledWith({
              uuid: pipelineSecurityReportFinding.uuid,
            });
          });

          it('should show a loading state when the mutation is in flight', async () => {
            const getMRButtonData = () =>
              findActionButtons()
                .props('buttons')
                .find((buttonData) => {
                  return buttonData.action === 'create-merge-request';
                });

            expect(getMRButtonData().loading).toBe(false);

            await findActionButtons().vm.$emit('create-merge-request');

            expect(getMRButtonData().loading).toBe(true);
          });

          it('should redirect to the merge request when the mutation is successful', async () => {
            expect(visitUrl).not.toHaveBeenCalled();

            findActionButtons().vm.$emit('create-merge-request');
            await waitForPromises();

            expect(visitUrl).toHaveBeenCalledWith(TEST_MERGE_REQUEST.webUrl);
          });
        });

        describe('error', () => {
          beforeEach(async () => {
            createWrapper({
              responseHandlers: {
                createMergeRequestMutation: jest
                  .fn()
                  .mockRejectedValue(new Error('mutation failed')),
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      vulnerability: {
                        mergeRequest: null,
                      },
                      remediations: [TEST_REMEDIATION],
                    },
                  }),
                ),
              },
            });

            await waitForPromises();
          });

          it('should show an error when the mutation fails', async () => {
            expect(findErrorAlert().exists()).toBe(false);

            await findActionButtons().vm.$emit('create-merge-request');
            await waitForPromises();

            expect(findErrorAlert().exists()).toBe(true);
          });
        });
      });
    });

    describe('gitlab issue creation', () => {
      const findCreateIssueButton = () => wrapper.findByTestId('create-issue-button');
      const clickCreateIssueButton = () => {
        return findCreateIssueButton().vm.$emit('click');
      };

      describe.each`
        description                                                        | userHasPermission | hasExistingIssue | shouldShowCreateIssueButton
        ${'user has permissions and there is no existing issue'}           | ${true}           | ${false}         | ${true}
        ${'user has permissions and there is an existing issue'}           | ${true}           | ${true}          | ${false}
        ${'user does not have permissions and there is no existing issue'} | ${false}          | ${false}         | ${false}
        ${'user does not have permissions and there is an existing issue'} | ${false}          | ${true}          | ${false}
      `(
        'when the $description',
        ({ userHasPermission, hasExistingIssue, shouldShowCreateIssueButton }) => {
          beforeEach(async () => {
            createWrapper({
              responseHandlers: {
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      vulnerability: {
                        id: '1',
                        userPermissions: {
                          createVulnerabilityFeedback: userHasPermission,
                        },
                      },
                      issueLinks: {
                        nodes: hasExistingIssue ? [{ issueType: 'CREATED' }] : [],
                      },
                    },
                  }),
                ),
              },
            });

            await waitForPromises();
          });

          it(`${
            shouldShowCreateIssueButton ? 'should' : 'should not'
          } show the "create issue" button`, () => {
            expect(findCreateIssueButton().exists()).toBe(shouldShowCreateIssueButton);
          });
        },
      );

      it('should show a loading state when the mutation is in flight', async () => {
        createWrapper();
        await waitForPromises();

        expect(findCreateIssueButton().props('loading')).toBe(false);

        await clickCreateIssueButton();

        expect(findCreateIssueButton().props('loading')).toBe(true);
      });

      it('should show an error when the mutation fails', async () => {
        createWrapper({
          responseHandlers: {
            createIssueMutation: jest.fn().mockRejectedValue(new Error('mutation failed')),
          },
        });
        await waitForPromises();

        expect(findErrorAlert().exists()).toBe(false);

        clickCreateIssueButton();
        await waitForPromises();

        expect(findErrorAlert().exists()).toBe(true);
      });

      it('should create a new issue and redirect to it when the mutation is successful', async () => {
        const createIssueMutationHandler = jest
          .fn()
          .mockResolvedValue(securityFindingCreateIssueMutationResponse);

        createWrapper({
          responseHandlers: {
            createIssueMutation: createIssueMutationHandler,
          },
        });
        await waitForPromises();

        expect(visitUrl).not.toHaveBeenCalled();
        expect(createIssueMutationHandler).not.toHaveBeenCalled();

        clickCreateIssueButton();
        await waitForPromises();

        expect(createIssueMutationHandler).toHaveBeenCalledWith({
          findingUuid: pipelineSecurityReportFinding.uuid,
          projectId: pipelineSecurityReportFinding.project.id,
        });
        expect(visitUrl).toHaveBeenNthCalledWith(
          1,
          securityFindingCreateIssueMutationResponse.data.securityFindingCreateIssue.issue.webUrl,
        );
      });
    });

    describe('Jira issue creation', () => {
      let createExternalIssueMutationHandler;

      const findCreateJiraIssueButton = () => wrapper.findByTestId('create-jira-issue-button');
      const clickCreateJiraIssueButton = () => {
        findCreateJiraIssueButton().vm.$emit('click');
        return nextTick();
      };

      describe('with Jira integration disabled', () => {
        beforeEach(async () => {
          createWrapper();
          await waitForPromises();
        });

        it('should not show the "create jira issue" button', () => {
          expect(findCreateJiraIssueButton().exists()).toBe(false);
        });
      });

      describe('with Jira integration enabled', () => {
        describe('with an existing Jira issue', () => {
          beforeEach(async () => {
            createWrapper({
              responseHandlers: {
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      issueLinks: {
                        nodes: [{ issueType: 'CREATED', externalIssue: { title: 'JIRA-123' } }],
                      },
                    },
                  }),
                ),
              },
            });
            await waitForPromises();
          });

          it('should not show the "create jira issue" button', () => {
            expect(findCreateJiraIssueButton().exists()).toBe(false);
          });
        });

        describe('success', () => {
          beforeEach(async () => {
            createExternalIssueMutationHandler = jest
              .fn()
              .mockResolvedValue(securityFindingCreateExternalIssueMutationResponse);

            createWrapper({
              responseHandlers: {
                createExternalIssueMutation: createExternalIssueMutationHandler,
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      project: {
                        hasJiraVulnerabilityIssueCreationEnabled: true,
                      },
                    },
                  }),
                ),
              },
            });
            await waitForPromises();
          });

          it('should call the mutation when the button is clicked', async () => {
            expect(createExternalIssueMutationHandler).not.toHaveBeenCalled();

            await clickCreateJiraIssueButton();

            expect(createExternalIssueMutationHandler).toHaveBeenCalledWith({
              vulnerabilityId: pipelineSecurityReportFinding.vulnerability.id,
            });
          });

          it('should show a loading state when the mutation is in flight', async () => {
            expect(findCreateJiraIssueButton().props('loading')).toBe(false);

            await clickCreateJiraIssueButton();

            expect(findCreateJiraIssueButton().props('loading')).toBe(true);
          });

          it('should redirect to the new issue when the mutation is successful', async () => {
            expect(visitUrl).not.toHaveBeenCalled();

            await clickCreateJiraIssueButton();
            await nextTick();
            await waitForPromises();

            expect(visitUrl).toHaveBeenCalledWith('https://jira.com/1', true);
          });
        });

        describe('error', () => {
          beforeEach(async () => {
            createWrapper({
              responseHandlers: {
                createExternalIssueMutation: jest
                  .fn()
                  .mockRejectedValue(new Error('mutation failed')),
                securityReportFindingQuery: jest.fn().mockResolvedValue(
                  getPipelineSecurityReportFindingResponse({
                    overrides: {
                      project: {
                        hasJiraVulnerabilityIssueCreationEnabled: true,
                      },
                    },
                  }),
                ),
              },
            });
            await waitForPromises();
          });

          it('should show an error when the mutation fails', async () => {
            expect(findErrorAlert().exists()).toBe(false);

            await clickCreateJiraIssueButton();
            await waitForPromises();

            expect(findErrorAlert().text()).toBe(
              'There was an error creating a Jira issue for the finding. Please try again.',
            );
          });
        });

        describe('polling', () => {
          const advanceToNextFetch = (milliseconds) => {
            jest.advanceTimersByTime(milliseconds);
          };

          describe('success', () => {
            beforeEach(async () => {
              const vulnerabilityQueryHandler = jest
                .fn()
                .mockResolvedValueOnce(
                  getVulnerabilityExternalIssuesQueryResponse({ externalIssues: [] }),
                )
                .mockResolvedValue(getVulnerabilityExternalIssuesQueryResponse());

              createWrapper({
                responseHandlers: {
                  vulnerabilityExternalIssuesQuery: vulnerabilityQueryHandler,
                  securityReportFindingQuery: jest.fn().mockResolvedValue(
                    getPipelineSecurityReportFindingResponse({
                      overrides: {
                        project: {
                          hasJiraVulnerabilityIssueCreationEnabled: true,
                        },
                      },
                    }),
                  ),
                },
              });
              await waitForPromises();
            });

            it('should poll the mutation until the issue is created', async () => {
              await clickCreateJiraIssueButton();
              await waitForPromises();

              expect(findCreateJiraIssueButton().props('loading')).toBe(true);
              expect(visitUrl).not.toHaveBeenCalled();

              advanceToNextFetch(VULNERABILITY_POLLING_INTERVAL);
              await waitForPromises();

              expect(findCreateJiraIssueButton().props('loading')).toBe(false);
              expect(visitUrl).toHaveBeenCalledWith('https://jira.com/1', true);
            });
          });

          describe('error', () => {
            beforeEach(async () => {
              const vulnerabilityQueryHandler = jest
                .fn()
                .mockResolvedValueOnce(
                  getVulnerabilityExternalIssuesQueryResponse({
                    externalIssues: [],
                  }),
                )
                .mockRejectedValue(new Error('query failed'));

              createWrapper({
                responseHandlers: {
                  vulnerabilityExternalIssuesQuery: vulnerabilityQueryHandler,
                  securityReportFindingQuery: jest.fn().mockResolvedValue(
                    getPipelineSecurityReportFindingResponse({
                      overrides: {
                        project: {
                          hasJiraVulnerabilityIssueCreationEnabled: true,
                        },
                      },
                    }),
                  ),
                },
              });
              await waitForPromises();
            });

            it('should stop the loading indicator and render an error message', async () => {
              await clickCreateJiraIssueButton();
              await waitForPromises();

              expect(findErrorAlert().exists()).toBe(false);
              expect(findCreateJiraIssueButton().props('loading')).toBe(true);

              advanceToNextFetch(VULNERABILITY_POLLING_INTERVAL);
              await waitForPromises();

              expect(findCreateJiraIssueButton().props('loading')).toBe(false);
              expect(findErrorAlert().text()).toBe(
                'There was an error creating a Jira issue for the finding. Please try again.',
              );
            });
          });
        });
      });
    });
  });
});
